\documentclass{article}
\usepackage[heading=true]{ctex}
\usepackage{xeCJK} %调用 xeCJK 宏包
\usepackage{fontspec}
\usepackage{graphicx}
\usepackage[a4paper,left=15mm,right=15mm,top=15mm,bottom=15mm]{geometry}
\usepackage{soul} 
\usepackage{color, xcolor}
\usepackage{listings}
\usepackage[hidelinks]{hyperref}

\setCJKmainfont[
				BoldFont=FandolSong-Bold.otf
]{FandolSong-Regular.otf}
\setCJKmonofont{FandolKai-Regular.otf}

\ctexset{section={name={,.},format={\Large\bfseries}}}

\begin{document}
%\setmainfont{RobotoMono-Regular.otf}
%\setmonofont{SourceCodePro-Regular.otf}

\title{基本TCP网络编程API}
\date{\today}

\maketitle
\newpage
\tableofcontents
\newpage

\section{TCP基础}

\subsection{TCP建立与关闭}
当tcp建立连接的时候，会选择一个tcp数据长度(MSS)，这个值一般是以IP层不分片为基准来搞的。

虽然书上说处于的2msl的socket对不能重复使用，但是看实际的，还是可以使用的。抓包中的tsval(本地时间戳)和tsecr(对端时间戳)可以区分来两次SYN的包。

\begin{figure}[h]
	\centering
	\includegraphics[width=15cm]{Screenshot_20230401_213623.png}
	\caption{端口复用机制测试}
\end{figure}

\begin{figure}[h]
	\centering
	\includegraphics[width=15cm]{tu1.png}
	\caption{反过来依然可以建立连接}
\end{figure}

上图中的包9是新的SYN包，SEQ同第一个包一样是0,但是依然通信成功了。所以双方开启地址复用的情况下可以用一对socket反复建立连接。但是同样可以发现，当双方端口反过来后，新SYN的tsecr变成了0。

\subsection{同时建立与关闭}
一种比较少见的情况是两端互为客户端服务器，同时建立连接与关闭连接。假设A用7777与B的8888建立连接，同时B也用8888和A的7777建立连接，这样的连接结果是只有一条通信链路。

\begin{figure}[h]
	\centering
	\includegraphics[width=15cm]{tu2.png}
	\caption{同时打开的时序}
\end{figure}
\begin{figure}[h]
	\centering
	\includegraphics[width=15cm]{tu3.png}
	\caption{同时关闭的时序}
\end{figure}

\subsection{拥塞控制与重传}

拥塞的表现：受到重复的ACK报文，或者确认定时器超时。原版的慢启动与拥塞避免描述如下：首先要将拥塞窗口设置为1，慢启动门限65535字节。
开始发送一个包，受到ack后发送两个，再发送四个(拥塞窗口一起指数变)，直到超过慢启动门限后拥塞窗口开始线性增加。当ack定时器超时，慢启动门限改为当前拥塞窗口的一半，拥塞窗口变成1,开始重新慢启动。如果仅仅是受到重复ack,那么慢启动门限仅仅减半，拥塞窗口还是处于线性增长状态(如果持续到出现超时，那么就会走慢启动)。

相比于超时，重复ack至少说明数据还在流转，只是可能网络拥塞或者就是ip曾乱序导致的重复ack。那么此时可以直接走快速重传与恢复。简单说就是收到了三个重复ack后立即回传丢失的报文，慢启动门限改为当前拥塞窗口的一半，拥塞窗口改为一半多三个包。然后每再收到一个重复ack后，拥塞窗口+1,并发送一个新数据包。等到新数据的ack来，拥塞窗口改为慢启动门限。开始走拥塞避免。

\section{Socket函数}
socket函数返回一个通信端点，同时返回一个文件描述符。返回指向该端点的文件描述符。成功调用返回的文件描述符将是进程当前未打开的编号最低的文件描述符。

int socket(int domain, int type, int protocol);\\

type参数除了SOCK\_STREAM之类的指定类型之外，还可以以位或的方式传入SOCK\_NONBLOCK以及SOCK\_CLOEXEC

注意如下错误：
\begin{enumerate}
	\item EMFILE：进程的文件描述符数量不足，可以ulimit命令放开数量。
	\item ENFILE：整个操作系统的描述符数量都不足了。
	\item ENOBUFS or ENOMEM：内存泄漏，需要排查
	
\end{enumerate}
{\rmfamily Linux\textbf{对文件的限制有两个层面}：全系统(/proc/sys/fs/file-max一般够用)}、用户(ulimt 可以修改/etc/security/limits.conf文件来增大用户的文件描述符限制)。
\\

\fcolorbox{blue}{yellow}{\parbox{0.5\textheight}{\begin{enumerate}
			\item socket函数本身就会创建一个fd。
			\item 注意上面提到的放开fd限制的方法。
			\item 当在一个已经断开连接上进行读写，导致SIGPIPE信号触发，进程崩溃。
\end{enumerate}}}


\section{Bind函数}
此函数唯一需要注意的是既可以客户端调用也可以服务端调用，可以分配一个地址。

\section{Connect函数}
{\ttfamily 当socket是一个TCP时，connect会触发发送第一个SYN分节。开始后续的三次握手过程。}
当socket是一个UDP时，connect后只能向connect的地址收发数据。connect后的udp接口可以使用
read、write来发送接收数据。而sendto函数每次发送都会拷贝一次地址，增大了大约三分之一的开销。


{\kaishu 当Connect失败的时候，不允许再次connect。需要close后用一个新的socket然后再次connect。}
\\

\fcolorbox{blue}{yellow}{\parbox{0.5\textheight}{\begin{enumerate}
			\item connect失败后一定要记得先close再创建然后再connect。
			\item UDP也可以connect,但是结果就是只能从一个地址收发数据，但是可以轻微提高性能。
\end{enumerate}}}

\section{Listen函数}
对于此函数，需要注意的是第二个参数。当SYN被服务器接收到，连接处于SYN\_RECV状态。此时这个连接被服务器放进了未完成队列。当三次握手完毕后，连接被
服务器放进完成连接队列，处于ESTABLISH状态。

\begin{lstlisting}
	int listen(int fd,int backlog);
\end{lstlisting}

backlog参数是完成队列的大小。如果要调整未完成连接的队列大小，/proc/sys/net/ipv4/tcp\_max\_syn\_backlog可以调整。当连接请求到达的时候，内核会创建一个TCP数据结构(tcb)。tcb还没有和fd关联，当accept的时候，就会产生一个fd和tcb关联起来。

\section{Accept函数}
如果socket设置为非阻塞，accept又没连接来，那么就会返回EAGIN或者EWOULDBLOCK。
当多路复用返回监听socket可读时，accept并不一定会立即返回。因为此时连接可能因为某种错误中断了。或当启用了多线程时，一个线程已经取走了连接。所以监听socket一般都是非阻塞的(这么说一般的socket都应该是非阻塞的，一般的反应堆模型对单个socket都是单线程的)。

\fcolorbox{blue}{yellow}{\parbox{0.5\textheight}{\begin{enumerate}
			\item 监听socket一定要是非阻塞的。
\end{enumerate}}}

\section{Close函数}
当所有的引用都close后，socket断开连接。

\section{recv以及send}
首先，recvfrom函数是从socket的接收队列里面读取了一整个缓冲区。而接收的时候也是按照一个ip整包一个队列项的方式加入到了socket接收队列里面的。因此不会存在一次回调出现
两个不同客户端数据一起回调的情况。


\section{SOCKET选项}

\begin{enumerate}
	\item SO\_RCVBUG：接收缓冲区的的大小，这个和TCP滑动窗口大小不是一个意思。
	设置或获取以字节为单位的最大套接字接收缓冲区。当使用setsockopt设置该值时，内核将该值加倍，并且这个加倍的值由getsockopt返回。默认值由“/proc/sys/net/core/rmem\_default”文件设置，最大值由“/proc/sys/net/core/rmem\_max”文件设置。这个选项的最小值是256。但是，一般的缓冲区大小都会比tcp窗口大。
	
\end{enumerate}



\section{EPOLL模型}

epoll API执行与poll类似的任务:监视多个文件描述符，以查看其中任何一个文件是否可以进行I/O操作。epoll API既可以用作边缘触发接口，也可以用作水平触发接口，并且可以很好地扩展到大量监视的文件描述符。

epoll API的核心概念是epoll实例，这是一个内核数据结构，从用户空间的角度来看，可以将其视为两个列表的容器:

兴趣列表(有时也称为epoll集):进程在监视中注册了感兴趣的文件描述符集。

就绪列表:一组为I/O准备好的文件描述符。就绪列表是兴趣列表中文件描述符的一个子集(或者更准确地说，是对这些文件描述符的一组引用)，内核会根据对这些文件描述符的I/O活动动态填充这些文件描述符。

如果多个线程(或进程，如果子进程在fork(2)上继承了epoll文件描述符)在epoll\_wait中被阻塞，等待相同的epoll文件描述符，并且兴趣列表中标记为边缘触发(EPOLLET)通知的文件描述符准备就绪，则只有一个线程(或进程)从epoll\_wait(2)中被唤醒。这提供了一个有用的优化，以避免在某些情况下“惊群”唤醒。

有几个问题需要抢先澄清：

\begin{enumerate}
	\item 区分注册入epoll中文件描述符的关键字是文件描述符号和打开文件描述符(也称为“打开文件句柄”，是打开文件的内核内部表示)的组合。
	
	\item 不可以重复注册fd到epoll,但是如果使用dup复制一个fd,然后用不同事件注册到epoll。那是可以的。
	
	\item epoll自己的fd不能注册到自己里面，但是可以注册到其他epoll中。
	
	\item 注意，使用边缘出发的时候，epoll可能会出现描述符饥饿的问题。官方给出的解决办法是应用自己维护一个列表，当收到就绪事件时加入列表。当读完后撤出列表。(但是不清楚，这样和使用水平出发有区别么？)
	
\end{enumerate}

下面是EPOLL支持的事件
\begin{enumerate}
	\item EPOLLIN/EPOLLOUT 可读可写
	
	\item EPOLLRDHUP 流式socket对端关闭了写端，或者直接干脆关闭了socket
	
	\item EPOLLHUP 对端socket非正常关闭
	
	\item EPOLLERR 出现错误，一般是向一个已经关闭的socket写数据导致
	
	\item EPOLLET 边沿触发
	
	\item EPOLLONESHOT 只执行一次，需要再次添加到epoll中
	
	\item EPOLLWEAKUP 设置这个标志，事件从接收到处理，一直都会处于唤醒状态(低功耗情况下需要留意) 驱动必须得有个CAP\_BLOCK\_SUSPEND能力
	
	\item EPOLLEXCLUSIVE 4.5内核才引入 。为附加到目标文件描述符fd的epoll文件描述符设置独占唤醒模式。当唤醒事件发生并且多个epoll文件描述符使用EPOLLEXCLUSIVE附加到同一个目标文件时，一个或多个epoll文件描述符将使用epoll\_wait(2)接收事件。在这个场景中(当没有设置EPOLLEXCLUSIVE时)，默认情况是所有epoll文件描述符都接收一个事件。因此，在某些情况下，EPOLLEXCLUSIVE可用于避免惊群问题。由于ET模式天然就避免了惊群，所以这个标志一般是LT模式用的。
	
	\item EPOLLPRI 高优先级数据	
\end{enumerate}



\fcolorbox{blue}{yellow}{\parbox{0.5\textheight}{\begin{enumerate}
			\item 边缘触发可以避免惊群
			\item 避免et饥饿
\end{enumerate}}}

\end{document}